// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;

namespace SnapObjects.Data
{
    /// <summary>
    ///     Represents the value for a SQL parameter. Once you want to execute a SQL statement and pass in 
    ///     the value for a SQL parameter, you can create an instance of the ParamValue class as an argument.
    /// </summary>
    public class ParamValue
    {
        private Type _dataType;

        private ParamValue(string name, object value)
        {
            this.Name = name;
            this.Value = value;
            this.Direction = ParameterDirection.Input;
        }

        internal ParamValue(string name, object value, Type dataType, ParameterDirection direction)
        : this(name, value, dataType, direction, 0, String.Empty)
        {
        }

        internal ParamValue(string name, object value, Type dataType, ParameterDirection direction, int size, string typeName)
            :this(name, value, dataType, direction, size, 0, 0, typeName)
        {
        }

        internal ParamValue(
            string name, object value, Type dataType, ParameterDirection direction, int size, byte precision, byte scale, string typeName)
        {
            _dataType = dataType;

            this.Value = value;
            this.Name = name;
            this.Direction = direction;
            this.Size = size;
            this.TypeName = typeName;
            this.Precision = precision;
            this.Scale = scale;
        }

        /// <summary>
        ///     Gets the name of the parameter.
        /// </summary>
        public string Name { get; }

        /// <summary>
        ///     Gets the value of the parameter.
        /// </summary>
        public object Value { get; set; }

        public int Size { get; }

        internal byte Scale { get;  }
        internal byte Precision { get; }
        internal string TypeName { get; set; }
        internal int[] ArrayBindSize { get; set; }

        /// <summary>
        ///     Gets the data type of the parameter value.
        /// </summary>
        public Type DataType
        {
            get
            {
                if (_dataType == null)
                {
                    if (this.Value != null)
                    {
                        _dataType = this.Value.GetType();
                    }
                    else
                    {
                        throw new InvalidOperationException("Parameter value is null, parameter type must be specified. Parameter name: " + this.Name);
                    }
                }

                return _dataType;
            }
        }

        /// <summary>
        ///     Gets the direction type of the parameter.
        /// </summary>
        public ParameterDirection Direction { get; }

        /// <summary>
        ///     Creates a new ParamValue object and specifies the name and value of the parameter. 
        ///     You can also specify the direction type of the parameter.The default is ParameterDirection.Input.
        /// </summary>
        /// <typeparam name="TType">The datatype of the parameter value.</typeparam>
        /// <param name="name">The name of the parameter.</param>
        /// <param name="value">The value of the parameter.</param>
        /// <param name="direction">
        ///     (Optional) The direction type of the parameter.
        ///     The default is ParameterDirection.Input.
        /// </param>
        /// <returns>Returns the newly created ParamValue object.</returns>
        public static ParamValue New<TType>(
            string name, TType value, ParameterDirection direction = ParameterDirection.Input)
        {
            return new ParamValue(name, value, typeof(TType), direction);
        }

        /// <summary>
        ///     Creates a new ParamValue object and specifies the name and value of the parameter.
        /// </summary>
        /// <param name="name">The name of the parameter.</param>
        /// <param name="value">The value of the parameter.</param>
        /// <returns>Returns the newly created ParamValue object.</returns>
        public static ParamValue New(string name, object value)
        {
            return new ParamValue(name, value);
        }

        public static ParamValue Input<TType>(string name, TType value)
        {
            return new ParamValue(name, value, typeof(TType), ParameterDirection.Input);
        }

        public static ParamValue Input(string name, object value, Type dataType)
        {
            return new ParamValue(name, value, dataType, ParameterDirection.Input);
        }

        public static ParamValue Output<TType>(string name)
        {
            return new ParamValue(name, null, typeof(TType), ParameterDirection.Output);
        }

        public static ParamValue Output<TType>(string name, int size)
        {
            return new ParamValue(
                name, null, typeof(TType), ParameterDirection.Output, size, String.Empty);
        }

        public static ParamValue Output(string name, Type dataType)
        {
            return new ParamValue(name, null, dataType, ParameterDirection.Output);
        }

        public static ParamValue Output(string name, int size, Type dataType)
        {
            return new ParamValue(
                name, null, dataType, ParameterDirection.Output, size, String.Empty);
        }

        public static ParamValue InputOutput(string name, object value, Type dataType)
        {
            return new ParamValue(name, value, dataType, ParameterDirection.InputOutput);
        }

        public static ParamValue InputOutput<TType>(string name, TType value)
        {
            return new ParamValue(name, value, typeof(TType), ParameterDirection.InputOutput);
        }

        public static ParamValue ReturnValue<TType>(string name)
        {
            return new ParamValue(name, null, typeof(TType), ParameterDirection.ReturnValue);
        }

        public static ParamValue ReturnValue(string name, Type dataType)
        {
            return new ParamValue(name, null, dataType, ParameterDirection.ReturnValue);
        }

        //Compatible with Inconsistent Argument Name and Actual Parameter Name
        internal static IList<IAdoDbParameter> ConvertParameters(
            ISqlGenerationHelper helper,
            IReadOnlyDictionary<string, IAdoDbParameter> parameters,
            object[] values)
        {
            var result = new List<IAdoDbParameter>();

            if (parameters.Count <= values.Length)
            {
                var sqlParams = values.OfType<ParamValue>()
                    .ToDictionary(m => m.Name, m => m, StringComparer.OrdinalIgnoreCase);

                var i = 0;

                foreach (var parameter in parameters)
                {
                    if (sqlParams.Count > 0)
                    {
                        if (sqlParams.TryGetValue(parameter.Key, out var param))
                        {
                            ((AdoDbParameter)parameter.Value).Value = param.Value;
                            ((AdoDbParameter)parameter.Value).ClrType = param.DataType;
                            ((AdoDbParameter)parameter.Value).Direction = param.Direction;
                        }
                        else
                        {
                            throw new ArgumentException($"No parameter '{parameter.Key}' found.");
                        }
                    }
                    else
                    {
                        ((AdoDbParameter)parameter.Value).Value = values[i];
                        ((AdoDbParameter)parameter.Value).ClrType =
                            parameter.Value.ClrType ?? typeof(object);
                    }

                    result.Add(parameter.Value);

                    i++;
                }
            }
            else
            {
                throw new ArgumentException("The number of parameters does not match.");
            }

            return result;
        }

        internal static IEnumerable<IAdoDbParameter> CreateProcedureParameters(
            string sqlText, DataContext context, object[] parameters, bool queryParameterSchema = true)
        {
            IReadOnlyList<IAdoDbParameter> dbParameters = new List<IAdoDbParameter>();

            if (queryParameterSchema &&
                parameters.Any(
                    m => m is ParamValue p &&
                    p.Direction != ParameterDirection.Input &&
                    (p.DataType == typeof(decimal) || p.DataType == typeof(decimal?))))
            {
                dbParameters = ProcedureSchemaManager.GetProcedureParameter(context, sqlText);
            }

            var adoParameters = CreateAdoDbParameter(context, dbParameters, parameters);

            return adoParameters;
        }

        internal static IEnumerable<IAdoDbParameter> CreateFunctionParameters(
            string sqlText, DataContext context, object[] parameters, bool queryParameterSchema = true)
        {
            IReadOnlyList<IAdoDbParameter> dbParameters = new List<IAdoDbParameter>();

            if (queryParameterSchema && parameters.Any(
                m => m is ParamValue p &&
                p.Direction != ParameterDirection.Input &&
                (p.DataType == typeof(decimal) || p.DataType == typeof(decimal?))))
            {
                var funcSchemas = context.AdoDbSchemaProvider.GetFunctionParameterSchema(sqlText);

                var funcSchema = funcSchemas.FirstOrDefault();

                dbParameters = funcSchema?.Parameters;
            }

            var adoParameters = CreateAdoDbParameter(context, dbParameters, parameters);

            return adoParameters;
        }

        internal static IEnumerable<ISqlParameter> CreateSqlParameter(DataContext context, object[] parameters)
        {
            var adoParameters = new List<ISqlParameter>();

            foreach (var item in parameters)
            {
                if (item is ParamValue param)
                {
                    adoParameters.Add(SqlBuilder.Parameter(param.Name, param.DataType, param.Direction));
                }
                else
                {
                    throw new ArgumentException(
                        "The stored procedure does not support simple parameters, use 'ParamValue.New'.");
                }
            }

            return adoParameters;
        }

        private static IEnumerable<IAdoDbParameter> CreateAdoDbParameter(
           DataContext context, IReadOnlyList<IAdoDbParameter> dbParameters, object[] parameters)
        {
            var adoParameters = new List<IAdoDbParameter>();

            var helper = context.AdoDbSqlProvider.SqlGenerationHelper;

            foreach (var item in parameters)
            {
                if (item is ParamValue param)
                {
                    var parameterName = param.Name;

                    if (!String.IsNullOrWhiteSpace(parameterName) && !parameterName.StartsWith(helper.ParameterIdentifier + ""))
                    {
                        parameterName = helper.GenerateParameterName(parameterName);
                    }

                    var adoDbParameter = new AdoDbParameter(param)
                    {
                        ParameterName = parameterName,
                    };

                    var dbParameter = dbParameters?.FirstOrDefault(
                        m => parameterName.Equals(m.ParameterName, StringComparison.OrdinalIgnoreCase));

                    if (dbParameter != null &&
                        dbParameter is AdoDbParameter parameter &&
                        parameter.Precision > 0)
                    {
                        adoDbParameter.Precision = parameter.Precision;
                        adoDbParameter.Scale = parameter.Scale;
                    }

                    adoParameters.Add(adoDbParameter);
                }
                else
                {
                    throw new ArgumentException(
                        "The stored procedure does not support simple parameters, use 'ParamValue.New'.");
                }
            }

            return adoParameters;
        }
    }
}
